<!DOCTYPE html>
<html lang="en">
<!--
Copyright [2024] [Michael Peter Christen, mc@yacy.net]

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

   http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Audio Grabber</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            text-align: center;
        }
        canvas {
            border: 1px solid black;
            margin: 20px auto;
            display: block;
        }
    </style>
</head>
<body>
    <h1>Audio Grabber</h1>
    <input type="text" id="serverhost" placeholder="Transcribe Host" value="localhost">
    <input type="text" id="serverport" placeholder="Transcribe Port" value="5040">
    <select id="audioInputSelect"></select>
    <button id="startBtn">Start</button>
    <button id="stopBtn" disabled>Stop</button>
    <canvas id="spectrogram" width="800" height="50"></canvas>
    <canvas id="volumeMeter" width="800" height="50"></canvas>

    <script>
        let audioContext;
        let mediaStream;
        let scriptProcessor;
        let buffer = [];
        let chunkId = Date.now().toString();
        let recording = false;
        let analyser;

        const RATE = 16000;
        const BUFFER_SIZE = 2 * 10 * RATE; // 10 seconds of audio
        const SILENCE_THRESHOLD = 500;

        const spectrogramCanvas = document.getElementById('spectrogram');
        const volumeMeterCanvas = document.getElementById('volumeMeter');
        const spectrogramCtx = spectrogramCanvas.getContext('2d');
        const volumeMeterCtx = volumeMeterCanvas.getContext('2d');

        document.getElementById('startBtn').addEventListener('click', startRecording);
        document.getElementById('stopBtn').addEventListener('click', stopRecording);

        // Get the list of audio input devices and populate the select element
        navigator.mediaDevices.enumerateDevices().then(devices => {
            const audioInputSelect = document.getElementById('audioInputSelect');
            const desktopOption = document.createElement('option');
            desktopOption.value = 'desktop';
            desktopOption.text = 'Desktop Audio';
            audioInputSelect.appendChild(desktopOption);

            devices.forEach(device => {
                if (device.kind === 'audioinput') {
                    const option = document.createElement('option');
                    option.value = device.deviceId;
                    option.text = device.label || `Microphone ${audioInputSelect.length + 1}`;
                    audioInputSelect.appendChild(option);
                }
            });
        });

        async function startRecording() {
            const selectedDeviceId = document.getElementById('audioInputSelect').value;

            if (selectedDeviceId === 'desktop') {
                try {
                    const stream = await navigator.mediaDevices.getDisplayMedia({ video: true, audio: true });
                    const audioTrack = stream.getAudioTracks()[0];
                    const audioOnlyStream = new MediaStream([audioTrack]);
                    startStream(audioOnlyStream);
                } catch (error) {
                    console.error('Error accessing desktop audio', error);
                }
            } else {
                const constraints = {
                    audio: {
                        deviceId: selectedDeviceId ? { exact: selectedDeviceId } : undefined,
                        sampleRate: RATE,
                    },
                };

                navigator.mediaDevices.getUserMedia(constraints)
                    .then(stream => {
                        startStream(stream);
                    })
                    .catch(error => console.error('Error accessing audio device', error));
            }
        }

        function startStream(stream) {
            audioContext = new (window.AudioContext || window.webkitAudioContext)({ sampleRate: RATE });
            mediaStream = stream;
            const mediaStreamSource = audioContext.createMediaStreamSource(stream);
            analyser = audioContext.createAnalyser();
            analyser.fftSize = 2048;
            scriptProcessor = audioContext.createScriptProcessor(4096, 1, 1);

            mediaStreamSource.connect(analyser);
            mediaStreamSource.connect(scriptProcessor);
            scriptProcessor.connect(audioContext.destination);
            scriptProcessor.onaudioprocess = processAudio;

            recording = true;
            document.getElementById('startBtn').disabled = true;
            document.getElementById('stopBtn').disabled = false;

            drawSpectrogram();
            drawVolumeMeter();
        }

        function stopRecording() {
            recording = false;
            mediaStream.getTracks().forEach(track => track.stop());
            scriptProcessor.disconnect();
            document.getElementById('startBtn').disabled = false;
            document.getElementById('stopBtn').disabled = true;
        }

        function processAudio(event) {
            const audioData = event.inputBuffer.getChannelData(0);
            if (isSilent(audioData)) {
                buffer = []; // Reset buffer
                chunkId = Date.now().toString(); // Get new chunk ID
            } else {
                buffer.push(...audioData);
            }

            if (buffer.length > 0) {
                sendChunk();
            }

            if (buffer.length >= BUFFER_SIZE) {
                buffer = []; // Reset buffer
                chunkId = Date.now().toString(); // Get new chunk ID
            }
        }

        function isSilent(data) {
            const maxVal = Math.max(...data);
            return maxVal < SILENCE_THRESHOLD / 32767; // Convert to 16-bit equivalent threshold
        }

        function sendChunk() {
            const int16Array = new Int16Array(buffer.map(n => n * 32767));
            const audioBuffer = new Blob([int16Array.buffer], { type: 'audio/wav' });
            const reader = new FileReader();
            reader.readAsDataURL(audioBuffer);
            reader.onloadend = () => {
                const base64data = reader.result.split(',')[1];
                const data = { chunk_id: chunkId, audio_b64: base64data };

                // construct the URL from the host and port
                const serverhost = document.getElementById('serverhost').value;
                const serverport = document.getElementById('serverport').value;
                const transcribeurl = `http://${serverhost}:${serverport}/transcribe`;
                fetch(transcribeurl, {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify(data)
                })
                    .then(response => {
                        if (response.ok) {
                            console.log(`Sent chunk ${chunkId} with ${buffer.length} samples`);
                        } else {
                            console.error(`Error sending chunk: ${response.status}:${response.statusText}`);
                        }
                    })
                    .catch(error => console.error('Error sending chunk:', error));
            };
        }

        function getColor(value) {
            const percent = value / 255;
            const red = Math.floor(Math.max(0, 255 * (percent - 0.5) * 2));
            const green = Math.floor(Math.max(0, 255 * (0.5 - Math.abs(percent - 0.5)) * 2));
            const blue = Math.floor(Math.max(0, 255 * (0.5 - percent) * 2));
            return `rgb(${red}, ${green}, ${blue})`;
        }

        function drawSpectrogram() {
            if (!recording) return;

            const freqData = new Uint8Array(analyser.frequencyBinCount);
            analyser.getByteFrequencyData(freqData);

            const width = spectrogramCanvas.width;
            const height = spectrogramCanvas.height;

            // Shift existing image to the left
            const imageData = spectrogramCtx.getImageData(1, 0, width - 1, height);
            spectrogramCtx.putImageData(imageData, 0, 0);

            // Draw new frequency data on the right
            for (let i = 0; i < height; i++) {
                const value = freqData[i];
                spectrogramCtx.fillStyle = getColor(value);
                spectrogramCtx.fillRect(width - 1, height - i, 1, 1);
            }

            requestAnimationFrame(drawSpectrogram);
        }

        function drawVolumeMeter() {
            if (!recording) return;

            const timeData = new Uint8Array(analyser.fftSize);
            analyser.getByteTimeDomainData(timeData);

            const width = volumeMeterCanvas.width;
            const height = volumeMeterCanvas.height;

            // Shift existing image to the left
            const imageData = volumeMeterCtx.getImageData(1, 0, width - 1, height);
            volumeMeterCtx.putImageData(imageData, 0, 0);

            // Calculate volume
            const volume = Math.sqrt(timeData.reduce((sum, value) => sum + Math.pow(value - 128, 2), 0) / timeData.length);
            const volumeHeight = (volume / 32) * height;

            // Clear the volume on the right with blue color
            volumeMeterCtx.fillStyle = 'grey';
            volumeMeterCtx.fillRect(width - 1, 0, 1, height);

            // Draw new volume level on the right
            volumeMeterCtx.fillStyle = 'black';
            volumeMeterCtx.fillRect(width - 1, height - volumeHeight, 1, volumeHeight);

            requestAnimationFrame(drawVolumeMeter);
        }
    </script>
</body>
</html>
